/*************************************************************************
 *
 *   file		: IRealmClient.cs
 *   copyright		: (C) The WCell Team
 *   email		: info@wcell.org
 *   last changed	: $LastChangedDate: 2009-06-13 08:08:28 +0100 (Sat, 13 Jun 2009) $
 *   last author	: $LastChangedBy: dominikseifert $
 *   revision		: $Rev: 967 $
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *************************************************************************/

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Cell.Core;
using NLog;
using WCell.Constants;
using WCell.Core;
using WCell.Core.Cryptography;
using WCell.PacketAnalysis;
using WCell.RealmServer.Debugging;
using WCell.RealmServer.Entities;
using WCell.RealmServer.Stats;
using WCell.Util.NLog;

namespace WCell.RealmServer.Network
{
    /// <summary>
    /// Represents a client connected to the realm server
    /// </summary>
    public sealed class RealmClient : ClientBase, IRealmClient
    {
        private static readonly Logger log = LogManager.GetCurrentClassLogger();

        private byte[] m_sessionKey;

        public static readonly List<IRealmClient> EmptyArray = new List<IRealmClient>();

        /// <summary>
        /// The server this client is connected to.
        /// </summary>
        public new RealmServer Server
        {
            get { return (RealmServer)_server; }
        }

        /// <summary>
        /// The <see cref="ClientInformation">system information</see> for this client.
        /// </summary>
        public ClientInformation Info { get; set; }

        /// <summary>
        /// The compressed addon data sent by the client.
        /// </summary>
        public byte[] Addons { get; set; }

        /// <summary>
        /// The account on this session.
        /// </summary>
        public RealmAccount Account { get; set; }

        /// <summary>
        /// The <see cref="Character" /> that the client is currently playing.
        /// </summary>
        public Character ActiveCharacter { get; set; }

        /// <summary>
        /// Whether or not this client is currently logging out.
        /// </summary>
        public bool IsOffline { get; set; }

        /// <summary>
        /// Whether or not communication with this client is encrypted.
        /// </summary>
        public bool IsEncrypted
        {
            get { return m_sessionKey != null; }
        }

        /// <summary>
        /// The local system uptime of the client.
        /// </summary>
        public uint ClientTime { get; set; }

        /// <summary>
        /// Connection latency between client and server.
        /// </summary>
        public int Latency { get; set; }

        /// <summary>
        /// The amount of time skipped by the client.
        /// </summary>
        /// <remarks>Deals with the the way we calculate movement delay.</remarks>
        public uint OutOfSyncDelay { get; set; }

        public uint LastClientMoveTime { get; set; }

        /// <summary>
        /// The client tick count.
        /// </summary>
        /// <remarks>It is set by opcodes 912/913, and seems to be a client ping sequence that is
        /// local to the map, and thus it resets to 0 on a map change.  Real usage isn't known.</remarks>
        public uint TickCount { get; set; }

        /// <summary>
        /// The client seed sent by the client during re-authentication.
        /// </summary>
        public uint ClientSeed { get; set; }

        /// <summary>
        /// The authentication message digest received from the client during re-authentication.
        /// </summary>
        public BigInteger ClientDigest { get; set; }

        /// <summary>
        /// Create an realm client for a given server.
        /// </summary>
        /// <param name="server">reference to the parent RealmServer</param>
        public RealmClient(RealmServer server)
            : base(server)
        {
        }

        /// <summary>
        /// Pass recieved data into the packet buffer and try to parse.
        /// </summary>
        /// <param name="numBytes">number of bytes waiting to be read</param>
        /// <returns>false, if there is a part of a packet still remaining</returns>
        protected override void OnReceive(BufferSegment segment, int numBytes)
        {
            byte[] recvBuffer = segment.Buffer.Array;

            var i = 1;
            do
            {
                // incomplete packet.
                if (numBytes < RealmPacketIn.HEADER_SIZE)
                    return;

                int packetLength;
                RealmServerOpCode opcode;

                int headerSize = GetContentInfo(recvBuffer, segment.Offset + _offset, out packetLength, out opcode);

                if (packetLength > WCellDef.MAX_CLIENT_PACKET_SIZE)
                {
                    // as far as we can tell, no packets are going to be over 30kb, let alone 130kb,
                    // so if we're going over here, it's 99.99999999% decryption breaking, so we will
                    // simply disconnect the player as they are screwed at this point.
                    LogUtil.ErrorException("Client {0} sent over-sized packet (ID: {1}) with size {2}kb, which exceeds maximum: {3}kb (packet #{4}, segment #{5})",
                              this, opcode, packetLength / 1024f,
                              WCellDef.MAX_CLIENT_PACKET_SIZE / 1024f, i, segment.Number);

                    Disconnect();

                    return;
                }

                packetLength += (headerSize - 4);

                // packet incomplete
                if (numBytes < packetLength)
                    return;

                var pkt = new RealmPacketIn(segment, _offset, packetLength, opcode, headerSize);

                //.UpdatePacketCounters(pkt.PacketId, fullPacketSize);

                PerformanceCounters.PacketsReceivedPerSecond.Increment();
                PerformanceCounters.TotalBytesReceived.IncrementBy(packetLength);

                RealmPacketMgr.Instance.HandlePacket(this, pkt);

                numBytes -= packetLength;
                _offset += packetLength;
                i++;
            } while (numBytes > 0);
        }

        /// <summary>
        /// Sends the given bytes representing a full packet, to the Client
        /// </summary>
        /// <param name="packet"></param>
        public override void Send(byte[] packet, int offset, int count)
        {
            if (IsOffline)
                return;

            PerformanceCounters.PacketsSentPerSecond.Increment();
            PerformanceCounters.TotalBytesSent.IncrementBy(count);

#if DEBUG
            DebugUtil.DumpPacketOut(Account, packet, offset, count, PacketSender.Server);
#endif

            if (IsEncrypted)
            {
                Encrypt(packet, offset);
            }

            base.Send(packet, offset, count);
        }

        public void Send(RealmPacketOut packet)
        {
            //_server.Debug(this, Resources.SendingPacket, packet, packet.Length);
            Send(packet.GetFinalizedPacket());
        }

        public override string ToString()
        {
            var infoStr = new StringBuilder();

            infoStr.Append(ClientAddress);
            infoStr.Append(":");
            infoStr.Append(Port);

            if (Account != null)
            {
                infoStr.Append(" - Account: ");
                infoStr.Append(Account.Name);
            }
            if (ActiveCharacter != null)
            {
                infoStr.Append(" - Char: ");
                infoStr.Append(ActiveCharacter.Name);
            }

            return infoStr.ToString();
        }

        public void Disconnect()
        {
            _server.DisconnectClient(this);
        }

        /// <summary>
        /// The session key for the latest session of this account.
        /// </summary>
        public byte[] SessionKey
        {
            get { return m_sessionKey; }
            set
            {
                m_sessionKey = value;
                m_packetCrypt = new PacketCrypt(value);
            }
        }

        #region Packet Encryption/Decryption

        private PacketCrypt m_packetCrypt;
        int encrypt;
        int decrypt;

        /// <summary>
        /// Encrypts the byte array
        /// </summary>
        /// <param name="data">The raw packet data to encrypt</param>
        private void Encrypt(byte[] data, int offset)
        {
            if (Interlocked.Exchange(ref encrypt, 1) == 1)
                log.Info("encrypt errorrrrr!");

            m_packetCrypt.Encrypt(data, offset, 4);
            Interlocked.Exchange(ref encrypt, 0);
        }

        /// <summary>
        /// Parses the header of a packet and obtains the length and opcode of the packet.
        /// </summary>
        /// <param name="inputData"></param>
        /// <param name="dataStartOffset"></param>
        /// <param name="packetLength"></param>
        /// <param name="opcode"></param>
        /// <returns>The offset of the start of the payload relative to <value>dataStartOffset</value></returns>
        private int GetContentInfo(byte[] inputData, int dataStartOffset, out int packetLength, out RealmServerOpCode opcode)
        {
            if (IsEncrypted)
            {
                return Decrypt(inputData, dataStartOffset, out packetLength, out opcode);
            }

            packetLength = inputData[dataStartOffset] << 8 | inputData[dataStartOffset + 1];

            // the opcode is actually 4 bytes, but can never go over 2, so we skip the last 2
            opcode = (RealmServerOpCode)(inputData[dataStartOffset + 2] | inputData[dataStartOffset + 3] << 8);

            return RealmPacketIn.HEADER_SIZE;
        }

        private int Decrypt(byte[] inputData, int dataStartOffset, out int packetLength, out RealmServerOpCode opcode)
        {
            byte firstByte = DecryptByte(inputData, dataStartOffset);

            // check for the big packet marker
            bool isLargePacket = (firstByte & 0x80) != 0;

            packetLength = 0;

            if (isLargePacket)
            {
                packetLength = (firstByte & 0x7F) << 16;
                packetLength |= DecryptByte(inputData, dataStartOffset + 1) << 8;
                packetLength |= DecryptByte(inputData, dataStartOffset + 2);

                opcode = (RealmServerOpCode)DecryptOpcode(inputData, dataStartOffset + 3);
            }
            else
            {
                packetLength |= firstByte << 8;
                packetLength |= DecryptByte(inputData, dataStartOffset + 1);

                opcode = (RealmServerOpCode)DecryptOpcode(inputData, dataStartOffset + 2);
            }

            return isLargePacket ? RealmPacketIn.LARGE_PACKET_HEADER_SIZE : RealmPacketIn.HEADER_SIZE;
        }

        private byte DecryptByte(byte[] inputData, int dataStartOffset)
        {
            if (Interlocked.Exchange(ref decrypt, 1) == 1)
                log.Info("decrypt errorrrrr!");

            m_packetCrypt.Decrypt(inputData, dataStartOffset, 1);

            Interlocked.Exchange(ref decrypt, 0);

            return inputData[dataStartOffset];
        }

        private int DecryptOpcode(byte[] inputData, int dataStartOffset)
        {
            m_packetCrypt.Decrypt(inputData, dataStartOffset, 4);
            return BitConverter.ToInt32(inputData, dataStartOffset);
        }

        #endregion
    }

    public class PacketCrypt
    {
        /// <summary>
        /// This is the key the client uses to encrypt its packets
        /// This is also the key the server uses to decrypt the packets
        /// </summary>
        private static readonly byte[] ServerDecryptionKey =
            {
                0xF4, 0x66, 0x31, 0x59, 0xFC, 0x83, 0x6E, 0x31,
                0x31, 0x02, 0x51, 0xD5, 0x44, 0x31, 0x67, 0x98
            };

        /// <summary>
        /// This is the key the client uses to decrypt server packets
        /// This is also the key the server uses to encrypt the packets
        /// </summary>
        private static readonly byte[] ServerEncryptionKey =
            {
                0x22, 0xBE, 0xE5, 0xCF, 0xBB, 0x07, 0x64, 0xD9,
                0x00, 0x45, 0x1B, 0xD0, 0x24, 0xB8, 0xD5, 0x45
            };

        // This is valid as HMAC-SHA1 transforms can be reused
        static readonly HMACSHA1 s_decryptClientDataHMAC = new HMACSHA1(ServerDecryptionKey);
        static readonly HMACSHA1 s_encryptServerDataHMAC = new HMACSHA1(ServerEncryptionKey);

        /// <summary>
        /// Encrypts data sent to the client
        /// </summary>
        private readonly ARC4 encryptServerData;
        /// <summary>
        /// Decrypts data sent from the client
        /// </summary>
        private readonly ARC4 decryptClientData;

        public PacketCrypt(byte[] sessionKey)
        {
            byte[] encryptHash = s_encryptServerDataHMAC.ComputeHash(sessionKey);
            byte[] decryptHash = s_decryptClientDataHMAC.ComputeHash(sessionKey);

            // Used by the client to decrypt packets sent by the server
            var decryptServerData = new ARC4(encryptHash); // CLIENT-SIDE
            // Used by the server to decrypt packets sent by the client
            decryptClientData = new ARC4(decryptHash); // SERVER-SIDE
            // Used by the server to encrypt packets sent to the client
            encryptServerData = new ARC4(encryptHash); // SERVER-SIDE
            // Used by the client to encrypt packets sent to the server
            var encryptClientData = new ARC4(decryptHash); // CLIENT-SIDE

            // Use the 2 encryption objects to generate a common starting point
            var syncBuffer = new byte[1024];
            encryptServerData.Process(syncBuffer, 0, syncBuffer.Length);
            encryptClientData.Process(syncBuffer, 0, syncBuffer.Length);

            // Use the 2 decryption objects to generate a common starting point
            syncBuffer = new byte[1024];
            decryptServerData.Process(syncBuffer, 0, syncBuffer.Length);
            decryptClientData.Process(syncBuffer, 0, syncBuffer.Length);
        }

        public void Decrypt(byte[] data, int start, int count)
        {
            decryptClientData.Process(data, start, count);
        }

        public void Encrypt(byte[] data, int start, int count)
        {
            encryptServerData.Process(data, start, count);
        }
    }
}